import 'package:meta/meta.dart';

import '../errors/update.dart';
import '../cache.dart';
import '../helpers/updates.dart';
import 'store.dart';

abstract class BaseCacheStore<T> extends CacheStore<T> {
  final _currentUpdates = <String, Future<CacheUpdate<T>>>{};

  @protected
  Future<Cache<T>> read(String key);

  @protected
  Future<List<Cache<T>>> readAll(List<String> keys);

  @override
  Future<Cache<T>> get(String key,
      {bool forceUpdate = false,
      bool forceCached = false,
      bool throwOnUpdateError = true,
      bool sharedUpdates = true,
      CacheUpdater<T> ifExpired}) async {
    final oldItem = await read(key);

    if (forceCached) {
      return oldItem;
    }

    CacheUpdate<T> update;

    if (sharedUpdates && _currentUpdates.containsKey(key)) {
      update = await _currentUpdates[key];
    } else {
      final future = updatedItemIfExpired(
        key: key,
        item: oldItem,
        updater: ifExpired,
        force: forceUpdate,
      );
      _currentUpdates[key] = future;
      update = await future;
      _currentUpdates.remove(key);
    }

    if (throwOnUpdateError && update is FailedUpdatedCache<T>) {
      throw CacheUpdateFailedException(update.cache, update.error);
    }

    if (update is UpdatedCache<T>) {
      await set(update.cache);
    }

    return update.cache;
  }

  @override
  Future<List<Cache<T>>> getAll(List<String> keys,
      {bool forceUpdate = false,
      bool forceCached = false,
      bool throwOnUpdateError = true,
      bool sharedUpdates = true,
      CacheUpdater<T> ifExpired}) async {
    final oldItems = await readAll(keys);

    if (forceCached) {
      return oldItems;
    }

    final futures = oldItems.asMap().entries.map((x) {
      final key = keys[x.key];
      if (sharedUpdates && _currentUpdates.containsKey(key)) {
        return _currentUpdates[key];
      } else {
        return updatedItemIfExpired(
          key: key,
          item: x.value,
          updater: ifExpired,
          force: forceUpdate,
        ).then((v) => _currentUpdates.remove(v.cache.key));
      }
    });

    final updates = await Future.wait(futures);

    final failedItems = updates
        .where((x) => x is FailedUpdatedCache<T>)
        .cast<FailedUpdatedCache<T>>()
        .toList();

    if (throwOnUpdateError && failedItems.isNotEmpty) {
      final errors =
          failedItems.map((x) => CacheUpdateFailedException(x.cache, x.error));
      throw CacheCollectionUpdateFailedException(errors.toList());
    }

    final updatedItems = updates
        .where((x) => x is UpdatedCache<T>)
        .cast<UpdatedCache<T>>()
        .toList();

    if (updatedItems.isNotEmpty) {
      await setAll(updatedItems.map((x) => x.cache).toList());
    }

    return updatedItems.map((x) => x.cache).toList();
  }

  @override
  Future<void> invalidate(String key) async {
    final oldItem = await read(key);
    if (oldItem != null) {
      await this.set(oldItem.invalidate());
    }
  }

  @override
  Future<void> invalidateAll(List<String> keys) async {
    final oldItems = await readAll(keys);
    final notNull =
        oldItems.where((x) => x != null).map((x) => x.invalidate()).toList();
    if (notNull.isNotEmpty) {
      await this.setAll(notNull);
    }
  }
}
